// BandsInTown Java API Client - BandsInTown service interface
// Copyright 2009 PriceFeeder Inc.  All rights reserved.
// http://code.google.com/p/bandsintown-java/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of PriceFeeder Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package com.pricefeeder.bandsintown;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.log4j.Logger;

import com.pricefeeder.bandsintown.model.Artist;
import com.pricefeeder.bandsintown.model.ErrorResponse;
import com.pricefeeder.bandsintown.model.Event;
import com.pricefeeder.bandsintown.model.Venue;
import com.pricefeeder.bandsintown.util.DateConverter;
import com.thoughtworks.xstream.XStream;

/**
 * Used to marshal/unmarshal XML service calls to the BandsInTown API.
 * 
 * This is the base class for all BandsIntown service request definitions
 * included in this package. The inheriting classes included in this client
 * library are: {@link ArtistsRequest}, {@link DailyRequest},
 * {@link RecommendedRequest}, {@link SearchRequest}.
 * <br /><br />
 * For more information about the BandsInTown API see 
 * {@link <a href="http://www.bandsintown.com/api/requests">http://www.bandsintown.com/api/requests</a>}
 * 
 * @author Eli Colner
 * 
 */
public abstract class BandsInTown {

	/**
	 * toggles logging
	 */
	protected static boolean DEBUG = false;

	/**
	 * Log4J logging instance
	 */
	protected Logger logger = Logger.getLogger(getClass());

	/**
	 * URI scheme of all service calls
	 */
	protected final String SERVICE_SCHEME = "http";

	/**
	 * BandsInTown API service call endpoint
	 */
	protected final String SERVICE_ENDPOINT = "//api.bandsintown.com";

	/**
	 * default response format.  Don't change or callService will break.
	 */
	protected final String RESPONSE_FORMAT = "xml";

	/**
	 * your API application ID
	 */
	protected static String applicationId;

	/**
	 * the buffered url of your service call.  This is built during the overriden preService method
	 */
	protected StringBuffer url;

	/**
	 * xstream XML response parser
	 */
	private static final XStream xstream = new XStream();

	static {
		xstream.registerConverter(new DateConverter());
		xstream.alias("events", ArrayList.class);
		xstream.alias("event", Event.class);
		xstream.alias("artists", ArrayList.class);
		xstream.alias("artist", Artist.class);
		xstream.alias("venue", Venue.class);
		xstream.alias("error_response", ErrorResponse.class);
		xstream.alias("errors", ArrayList.class);
		xstream.alias("error", String.class);
	}

	/**
	 * static properties loaded from bandsintown.properties file at the root of your project
	 */
	private static Properties properties = new Properties();

	static {
		try {
			properties.load(new FileInputStream("bandsintown.properties"));
			DEBUG = (properties.get("debug") == null) ? false : new Boolean(properties.get("debug").toString());
			applicationId = (properties.getProperty("app_id") == null) ? null : properties.getProperty("app_id").toString();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Override to inject logic just prior to service call
	 * 
	 * @return answer to question: "proceed with the service call?"
	 */
	abstract protected void preService() throws PreServiceException;

	/**
	 * Override to inject logic just after service call
	 */
	abstract protected void postService() throws PostServiceException;

	/**
	 * Performs remote service call to the BandsInTown api endpoint.
	 * 
	 * @throws java.io.IOException
	 *             communication error occurred
	 * @throws ServiceException
	 *             BandsInTown internal service error occurred
	 * @throws java.net.URISyntaxException
	 *             malformed {@link URI} built by instance fields during
	 *             inherited preService method
	 */
	@SuppressWarnings("unchecked")
	public final BandsInTownResponse callService() throws IOException, ServiceException,
			URISyntaxException {

		preService();

		HttpURLConnection conn = null;
		try {
			URL u = new URL(new URI(SERVICE_SCHEME, url.toString(), null)
					.toString());

			if (DEBUG) {
				if (logger == null)
					logger = Logger.getLogger(getClass());
				logger.info("calling endpoint: " + u);
			}

			conn = (HttpURLConnection) u.openConnection();

			InputStream in = null;
			if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
				in = conn.getErrorStream();
			} else {
				in = conn.getInputStream();
			}

			Object result = null;
			if (DEBUG) {
				String content = readAll(in);
				result = xstream.fromXML(content);
			} else {
				result = xstream.fromXML(in);
			}

			if (result instanceof List) {
				return new BandsInTownResponse((List<Event>) result);
			} else if (result instanceof ErrorResponse) {
				throw new ServiceException((ErrorResponse) result);
			} else {
				throw new ServiceException(conn.getContent().toString());
			}
		} finally {
			if (conn != null)
				conn.disconnect();

			postService();
		}
	}

	/**
	 * Reads an InputStream line by line and returns the String representation
	 * 
	 * @param in
	 *            an {@link java.io.InputStream} with XML data from api endpoint
	 *            connection
	 * @return the {@link java.lang.String} representation of the
	 *         {@link java.io.InputStream}
	 * @throws java.io.IOException
	 *             occurrs during an error while reading the
	 *             {@link java.io.InputStream}
	 */
	private String readAll(InputStream in) throws IOException {
		StringBuffer output = new StringBuffer();
		BufferedReader buffer = new BufferedReader(new InputStreamReader(in));
		String inputLine;
		while ((inputLine = buffer.readLine()) != null) {
			if (DEBUG) {
				logger.info(inputLine);
			}
			output.append(inputLine);
		}
		in.close();
		return output.toString();
	}

	/**
	 * Returns the application ID used for your service call.
	 * 
	 * @return your application ID
	 */
	public String getApplicationId() {
		return applicationId;
	}

	/**
	 * Sets the application ID used for your service call.  A word to identify your application or company
	 * 
	 * @param applicationId a unique identifier for your application
	 */
	public void setApplicationId(String applicationId) {
		BandsInTown.applicationId = applicationId;
	}

	/**
	 * Check to see if this call will run in debug mode (for logging purposes)
	 * @return true if debugging, else false
	 */
	public boolean isDebug() {
		return DEBUG;
	}

	/**
	 * Set debugging mode (for logging purposes)
	 * @param debug true if debugging, else false
	 */
	public void setDebug(boolean debug) {
		DEBUG = debug;
	}
}
